// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using SnapObjects.Commons;

namespace SnapObjects.Expressions
{
    internal abstract class MethodMatcher<TData, TParam>
    {
        protected abstract TData CreateDefault(Type type, object defaultValue);

        protected abstract TData CreateParams(Type type, List<TData> initializers);

        protected abstract TData ConvertType(TData argExpr, Type type);

        protected abstract bool MatchDefaultParameter(
            ParserOptions<TData> options, TParam paramExpr, out TData outExp);

        protected abstract Type GetParamExprType(TParam paramExpr);

        protected abstract Type GetArgExprType(TData argExpr);

        protected abstract TData CharToString(TData argExpr);

        public bool MatchMethod(
            ParserOptions<TData> options,
            MethodDataInfo<TData, TParam> info,
            TData[] args,
            out List<TData> paramList)
        {
            int j = 0, k = 0;
            paramList = new List<TData>();

            for (var i = 0; i < info.Parameters.Count; i++)
            {
                var item = info.Parameters[i];
                var itemValueType = this.GetParamExprType(item.Value);

                if (i == info.VariableParamIndex && j >= args.Length)
                {
                    var param = this.CreateDefault(itemValueType, item.Default);
                    if (param != null)
                    {
                        paramList.Add(param);
                        k++;
                    }

                    break;
                }

                if (j >= args.Length && !(j == args.Length && j == 0))
                {
                    return false;
                }

                if (info.VariableParamIndex == i && itemValueType.IsArray)
                {
                    var arrObjects = new List<TData>();
                    var arrType = itemValueType.GetElementType();
                    var isFirst = true;

                    while (j < args.Length)
                    {
                        var arg = args[j];
                        var argType = this.GetArgExprType(arg);

                        if (arrType != argType)
                        {
                            if (itemValueType.IsAnyType())
                            {
                                arrType = typeof(object);
                            }
                            else if (arrType.IsNullable() || arrType.IsImplicitFrom(argType))
                            {
                                arg = this.ConvertType(arg, arrType);
                            }
                            else
                            {
                                if (isFirst)
                                {
                                    return false;
                                }

                                throw new ArgumentException(item.Name);
                            }
                        }

                        arrObjects.Add(arg);

                        j++;
                    }

                    var arrArgs = this.CreateParams(arrType, arrObjects);

                    paramList.Add(arrArgs);
                }
                else
                {
                    if (this.MatchDefaultParameter(options, item.Value, out var exp))
                    {
                        paramList.Add(exp);

                        k++;
                    }
                    else if (args.Length > j &&
                        this.MatchParameter(options, item.Value, args[j], out exp))
                    {
                        paramList.Add(exp);

                        j++;
                    }
                    else
                    {
                        return false;
                    }
                }
            }

            if (info.VariableParamIndex > -1)
            {
                return (args.Length + k) >= paramList.Count;
            }
            else
            {
                return (args.Length + k) == paramList.Count;
            }
        }

        private bool MatchParameter(
            ParserOptions<TData> options, TParam paramExpr, TData argExpr, out TData outExp)
        {
            outExp = default;

            if (argExpr == null)
            {
                return false;
            }

            var argExprType = this.GetArgExprType(argExpr);
            var paramExprType = this.GetParamExprType(paramExpr);

            if (typeof(char).IsAssignableFrom(argExprType) && paramExprType == typeof(string))
            {
                outExp = this.CharToString(argExpr);

                return true;
            }

            if (paramExprType.IsImplicitFrom(argExprType))
            {
                if (paramExprType != argExprType)
                {
                    outExp = this.ConvertType(argExpr, paramExprType);
                }
                else
                {
                    outExp = argExpr;
                }
            }
            else if (argExprType == typeof(object))
            {
                outExp = this.ConvertType(argExpr, paramExprType);
            }

            return outExp != null;
        }
    }
}
